using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;

namespace Fine.CRM.Common
{

    /// <summary>
    /// Extensions for the IOrganizationService class to provide a set of common functions.
    /// </summary>
    internal static class IOrganizationServiceExtension
    {
        /// <summary>
        /// Attach a note to the referenced Entity.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="Regarding">EntityReference of the Entity that the note will be attached to.</param>
        /// <param name="Subject">Subject line of the note</param>
        /// <param name="Text">Text of the note</param>
        public static void AttachNote(this IOrganizationService OrganizationService, EntityReference Regarding, string Subject, string Text)
        {
            Entity note = new Entity("annotation");
            note.Attributes.Add("subject", Subject);
            note.Attributes.Add("notetext", Text);
            note.Attributes.Add("objectid", Regarding);
            OrganizationService.Create(note);
        }


        /// <summary>
        /// Attach a note to the referenced Entity.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="EntityLogicalName">Logicl name of the Entity.</param>
        /// <param name="Id">GUID of the Entity</param>
        /// <param name="Subject">Subject line of the note.</param>
        /// <param name="Text">Text of the note.</param>
        public static void AttachNote(this IOrganizationService OrganizationService, string EntityLogicalName, Guid Id, string Subject, string Text)
        {
            EntityReference regarding = new EntityReference(EntityLogicalName, Id);
            OrganizationService.AttachNote(regarding, Subject, Text);
        }


        public static void UploadAttachement(this IOrganizationService OrganizationService, EntityReference RegardingId, string FileName, string Base64DocumentBody, string MimeType)
        {
            Entity note = new Entity("annotation");
            note.Attributes.Add("subject", FileName);
            note.Attributes.Add("objectid", RegardingId);
            note.Attributes.Add("filename", FileName);
            note.Attributes.Add("documentbody", Base64DocumentBody);
            note.Attributes.Add("mimetype", MimeType);
            OrganizationService.Create(note);
        }


        /// <summary>
        /// Returns a reference to the CRM Currency record identified by the specified currency code.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="currencyCode"></param>
        /// <returns></returns>
        public static EntityReference GetCurrencyByCode(this IOrganizationService OrganizationService, string currencyCode)
        {
            EntityReference rv = null;

            QueryByAttribute qry = new QueryByAttribute("transactioncurrency");
            qry.ColumnSet = new ColumnSet(new string[] { "transactioncurrencyid" });
            qry.AddAttributeValue("isocurrencycode", currencyCode);
            qry.AddAttributeValue("statecode", 0);

            EntityCollection results = OrganizationService.RetrieveMultiple(qry);
            if (results.Entities.Count == 1)
            {
                rv = results.Entities[0].ToEntityReference();
            }

            return rv;
        }

        /// <summary>
        /// Returns a reference to the CRM Currency record identified by USD currency code.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <returns></returns>
        public static EntityReference GetCurrencyUSD(this IOrganizationService OrganizationService)
        {
            return GetCurrencyByCode(OrganizationService, "USD");
        }

        /// <summary>
        /// Retrieves an entity record from the CRM database based on the passed in Entity Reference and returns
        /// the columns specified. Columns can be specified as a ColumnSet object, a string array, or a delimited 
        /// string. If Columns are not provided, then all available columns will be retrieved.
        /// </summary>
        /// <param name="service"></param>
        /// <param name="Reference"></param>
        /// <param name="Columns"></param>
        /// <returns></returns>
        public static Entity GetEntity(this IOrganizationService service, EntityReference Reference, 
            ColumnSet Columns)
        {
            return service.Retrieve(Reference.LogicalName, Reference.Id, Columns);
        }

        public static Entity GetEntity(this IOrganizationService service, EntityReference Reference, 
            string[] Columns)
        {
            return GetEntity(service, Reference, new ColumnSet(Columns));
        }

        public static Entity GetEntity(this IOrganizationService service, EntityReference Reference, 
            string Columns)
        {
            string[] cols = Columns.Split(new char[] { ',', ';' });
            return GetEntity(service, Reference, cols);
        }

        public static Entity GetEntity(this IOrganizationService service, EntityReference Reference)
        {
            return GetEntity(service, Reference, new ColumnSet(true));
        }

        /// <summary>
        /// Returns an entity based on the target with additional column attributres merged in from the database if a 
        /// the attribues do not aleady exist in the target entity. Columns can be specified as a ColumnSet object, a 
        /// string array, or a deliminated string. If Columns are not provided, then all available columns 
        /// will be included in the merged record.
        /// </summary>
        /// <param name="service"></param>
        /// <param name="Target"></param>
        /// <param name="Columns"></param>
        /// <returns></returns>
        public static Entity GetMergedRecord(this IOrganizationService service, Entity Target, ColumnSet Columns)
        {
            //create a new entity record and merge the target data into the new record
            Entity result = new Entity(Target.LogicalName);
            result.Id = Target.Id;
            result.MergeWith(Target);

            //merge the specified columns from the database into the new record.
            result.MergeWith(service.GetEntity(Target.ToEntityReference(), Columns));

            return result;
        }

        public static Entity GetMergedRecord(this IOrganizationService service, Entity Target, string[] Columns)
        {
            return GetMergedRecord(service, Target, new ColumnSet(Columns));
        }

        public static Entity GetMergedRecord(this IOrganizationService service, Entity Target, string Columns)
        {
            string[] cols = Columns.Split(new char[] { ',', ';' });
            return GetMergedRecord(service, Target, cols);
        }

        public static Entity GetMergedRecord(this IOrganizationService service, Entity Target)
        {
            return GetMergedRecord(service, Target, new ColumnSet(true));
        }

        /// <summary>
        /// Grants a specific user Read Access to a specific Entity record.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="Record">EntityReference of the record.</param>
        /// <param name="UserId">GUID of user that is getting access.</param>
        public static void GrantUserReadAccess(this IOrganizationService OrganizationService, EntityReference Record, Guid UserId)
        {

            PrincipalAccess principalAccess = new PrincipalAccess();
            principalAccess.Principal = new EntityReference("systemuser", UserId);
            principalAccess.AccessMask = AccessRights.ReadAccess;

            GrantAccessRequest grantAccessRequest = new GrantAccessRequest();
            grantAccessRequest.Target = Record;
            grantAccessRequest.PrincipalAccess = principalAccess;

            GrantAccessResponse grantAccessResponse = (GrantAccessResponse)OrganizationService.Execute(grantAccessRequest);
        }

        public static void GrantReadAccess(this IOrganizationService OrganizationService, EntityReference Record, EntityReference Principal)
        {

            PrincipalAccess principalAccess = new PrincipalAccess();
            principalAccess.Principal = Principal;
            principalAccess.AccessMask = AccessRights.ReadAccess;

            GrantAccessRequest grantAccessRequest = new GrantAccessRequest();
            grantAccessRequest.Target = Record;
            grantAccessRequest.PrincipalAccess = principalAccess;

            GrantAccessResponse grantAccessResponse = (GrantAccessResponse)OrganizationService.Execute(grantAccessRequest);
        }

        public static void GrantAccess(this IOrganizationService OrganizationService, EntityReference Record, EntityReference Principal, AccessRights AccessRights)
        {
            PrincipalAccess principalAccess = new PrincipalAccess();
            principalAccess.Principal = Principal;
            principalAccess.AccessMask = AccessRights;

            GrantAccessRequest grantAccessRequest = new GrantAccessRequest();
            grantAccessRequest.Target = Record;
            grantAccessRequest.PrincipalAccess = principalAccess;

            GrantAccessResponse grantAccessResponse = (GrantAccessResponse)OrganizationService.Execute(grantAccessRequest);
        }

        public static AccessRights GetAccessRights(this IOrganizationService OrganizationService, EntityReference Record, EntityReference Principal)
        {
            RetrievePrincipalAccessRequest req = new RetrievePrincipalAccessRequest
            {
                Principal = Principal,
                Target = Record
            };

            RetrievePrincipalAccessResponse resp = (RetrievePrincipalAccessResponse)OrganizationService.Execute(req);

            return resp.AccessRights;
        }

        public static bool HasWriteAccess(this IOrganizationService OrganizationService, EntityReference Record, EntityReference Principal)
        {
            AccessRights rights = OrganizationService.GetAccessRights(Record, Principal);

            return (rights & AccessRights.WriteAccess) != AccessRights.None;
        }

        /// <summary>
        /// Evaluates users role assignments and returns true if the user is assigned one or more of the role names provided.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="UserId">GUID of the user.</param>
        /// <param name="RoleNames">Name of the roles supplied in a string array.</param>
        /// <returns></returns>
        public static bool IsAssignedRole(this IOrganizationService OrganizationService, Guid UserId, string[] RoleNames)
        {
            //qry all roles linked to the user and return the name of the role
            QueryExpression qry = new QueryExpression()
            {
                EntityName = "role",
                ColumnSet = new ColumnSet(new string[] { "name" }),

                LinkEntities = {
                    new LinkEntity
                    {
                        JoinOperator = JoinOperator.Inner,
                        LinkFromEntityName = "role",
                        LinkFromAttributeName = "roleid",
                        LinkToEntityName = "systemuserroles",
                        LinkToAttributeName = "roleid",

                        LinkCriteria = new FilterExpression
                        {
                            FilterOperator = LogicalOperator.And,
                            Conditions = 
                            {
                                new ConditionExpression("systemuserid",ConditionOperator.Equal,UserId)
                            }
                        }
                    }
                }
            };

            //create a filter to retrieve one or more role names
            FilterExpression roleFilter = new FilterExpression
            {
                IsQuickFindFilter = false,
                FilterOperator = LogicalOperator.Or,
            };

            foreach (string name in RoleNames)
            {
                roleFilter.Conditions.Add(new ConditionExpression("name", ConditionOperator.Equal, name));
            }

            //set the qry filter equal to the role filter
            qry.Criteria = roleFilter;

            EntityCollection result = OrganizationService.RetrieveMultiple(qry);

            return (result.Entities.Count >= 1);
        }


        /// <summary>
        /// Evaluates users role assignments and returns true if the user is assigned one or more of the role names provided.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="UserId">GUID of the user.</param>
        /// <param name="RoleName">Name of the role</param>
        /// <returns></returns>
        public static bool IsAssignedRole(this IOrganizationService OrganizationService, Guid UserId, string RoleName)
        {
            //convert the single role name to an array and call the overloaded function.
            string[] roleNames = new string[] { RoleName };
            return OrganizationService.IsAssignedRole(UserId, roleNames);
        }


        /// <summary>
        /// Evaluates a users membership in a team and returns true if the user is a team member.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="SystemUser">EntityReference of the user.</param>
        /// <param name="Team">EntityReference of the team.</param>
        /// <returns></returns>
        public static bool IsTeamMember(this IOrganizationService OrganizationService, EntityReference SystemUser, EntityReference Team)
        {
            if (SystemUser == null || SystemUser.LogicalName != "systemuser")
            {
                throw new Exception("SystemUser parameter must represent a CRM SystemUser record.");
            }

            if (Team == null || Team.LogicalName != "team")
            {
                throw new Exception("Team parameter must represent a team.");
            }

            QueryByAttribute qry = new QueryByAttribute("teammembership");
            qry.AddAttributeValue("teamid", Team.Id);
            qry.AddAttributeValue("systemuserid", SystemUser.Id);
            qry.ColumnSet = new ColumnSet(new string[] { "teammembershipid" });

            EntityCollection results = OrganizationService.RetrieveMultiple(qry);
            if (results.Entities.Count > 0)
            {
                return true;
            }

            return false;
        }


        public static bool IsTeamMember(this IOrganizationService OrganizationService, EntityReference SystemUser, string Team)
        {
            if (SystemUser == null || SystemUser.LogicalName != "systemuser")
            {
                throw new Exception("SystemUser parameter must represent a CRM SystemUser record.");
            }

            if (string.IsNullOrEmpty(Team))
            {
                throw new Exception("Team parameter must not be null.");
            }

            QueryExpression qry = new QueryExpression()
            {
                EntityName = "teammembership",
                Criteria =
                {
                    FilterOperator = LogicalOperator.And,
                    Conditions = {
                        new ConditionExpression("systemuserid",ConditionOperator.Equal, SystemUser.Id)
                    }
                },
                LinkEntities = {
                    new LinkEntity {
                        JoinOperator = Microsoft.Xrm.Sdk.Query.JoinOperator.Inner,
                        LinkFromEntityName = "teammembership",
                        LinkFromAttributeName = "teamid",
                        LinkToEntityName = "team",
                        LinkToAttributeName = "teamid",
                        LinkCriteria = {
                            FilterOperator  = LogicalOperator.And,
                            Conditions = {
                                new ConditionExpression("name",ConditionOperator.Equal,Team)
                            }
                        }
                    }
                }
            };

            EntityCollection results = OrganizationService.RetrieveMultiple(qry);
            if (results.Entities.Count > 0)
            {
                return true;
            }

            return false;

        }


        /// <summary>
        /// Sets the state of a record to the State Code and Status Code specified.
        /// </summary>
        /// <param name="OrganizationService"></param>
        /// <param name="Record">EntityReference of the record being modified.</param>
        /// <param name="StateCode">StateCode value as an integer</param>
        /// <param name="StatusCode">StatusCode value as an integer</param>
        public static void SetState(this IOrganizationService OrganizationService, EntityReference Record, int StateCode, int StatusCode)
        {
            SetStateRequest setStateReq = new SetStateRequest();
            setStateReq.EntityMoniker = Record;
            setStateReq.State = new OptionSetValue(StateCode);
            setStateReq.Status = new OptionSetValue(StatusCode);
            OrganizationService.Execute(setStateReq);
        }


        public static List<EntityReference> GetAssociatedRecords(this IOrganizationService OrganizationService, EntityReference TargetRecord, Relationship Relationship, string RelatedEntityLogicalName)
        {
            List<EntityReference> relatedItems = new List<EntityReference>();

            QueryExpression qry = new QueryExpression();
            qry.EntityName = RelatedEntityLogicalName;
            qry.ColumnSet = new ColumnSet(new string[] { RelatedEntityLogicalName + "id" });
           
            RelationshipQueryCollection relationshipQueryCollection = new RelationshipQueryCollection();
            relationshipQueryCollection.Add(Relationship, qry);

            RetrieveRequest retrieveRequest = new RetrieveRequest();
            retrieveRequest.RelatedEntitiesQuery = relationshipQueryCollection;
            retrieveRequest.ColumnSet = new ColumnSet(new string[] { TargetRecord.LogicalName + "id" });
            retrieveRequest.Target = TargetRecord;

            RetrieveResponse retrieveResponse = (RetrieveResponse)OrganizationService.Execute(retrieveRequest);

            if (retrieveResponse.Entity.RelatedEntities.Contains(Relationship))
            {
                foreach (Entity item in retrieveResponse.Entity.RelatedEntities[Relationship].Entities)
                {
                    relatedItems.Add(item.ToEntityReference());
                }
            }

            return relatedItems;
        
        }

    }

    /// <summary>
    /// Extensions for the IOrganizationService class to provide a set of common functions for accessing CRM MetaData.
    /// </summary>
    internal static class IOganizationServiceMetaDataExtension
    {
        /// <summary>
        /// Returns a list of strings that represent each metadata attribute
        /// for the specified entity schema name that can be used in a create operation. 
        /// </summary>
        /// <param name="service"></param>
        /// <param name="EntityName"></param>
        /// <returns></returns>
        public static List<string> GetMetaDataCreateFieldNames(this IOrganizationService orgService, string entityLogicalName)
        {
            List<string> fieldNameList = new List<string>();

            AttributeMetadata[] attributeMetadataArray = RetrieveAttributeMetaData(orgService, entityLogicalName);

            foreach (AttributeMetadata item in attributeMetadataArray)
            {
                if ((bool)item.IsValidForCreate)
                {
                    fieldNameList.Add(item.LogicalName);
                }
            }

            return fieldNameList;
        }

        /// <summary>
        /// Returns a list of strings that represent each metadata attribute
        /// for the specified entity schema name that can be used in an update operation. 
        /// </summary>
        /// <param name="service"></param>
        /// <param name="EntityName"></param>
        /// <returns></returns>
        public static List<string> GetMetaDataUpdateFieldNames(this IOrganizationService orgService, string entityLogicalName)
        {
            List<string> fieldNameList = new List<string>();

            AttributeMetadata[] attributeMetadataArray = RetrieveAttributeMetaData(orgService, entityLogicalName);

            foreach (AttributeMetadata item in attributeMetadataArray)
            {
                if ((bool)item.IsValidForUpdate)
                {
                    fieldNameList.Add(item.LogicalName);
                }
            }

            return fieldNameList;
        }


        /// <summary>
        /// Returns the text associated with specified optionset value for the identified entity and attribute.
        /// </summary>
        /// <param name="orgService"></param>
        /// <param name="entityLogicalName">Schema name of the entity that contains the optionset attribute.</param>
        /// <param name="attributeName">Schema name of the attribute.</param>
        /// <param name="selectedValue">Numeric value of the optionset.</param>
        /// <returns></returns>
        public static string GetOptionSetLabel(this IOrganizationService orgService, string entityLogicalName, string attributeName, int optionSetValue)
        {
            string returnLabel = string.Empty;

            OptionMetadataCollection optionsSetLabels = null;

            optionsSetLabels = RetrieveOptionSetMetaDataCollection(orgService, entityLogicalName, attributeName);

            foreach (OptionMetadata optionMetdaData in optionsSetLabels)
            {
                if (optionMetdaData.Value == optionSetValue)
                {
                    returnLabel = optionMetdaData.Label.UserLocalizedLabel.Label;
                    break;
                }
            }

            return returnLabel;
        }

        /// <summary>
        /// Returns an array of AttributeMetadata for the specified entity name.
        /// </summary>
        /// <param name="orgService"></param>
        /// <param name="entityLogicalName"></param>
        /// <returns></returns>
        public static AttributeMetadata[] RetrieveAttributeMetaData(this IOrganizationService orgService, string entityLogicalName)
        {
            RetrieveEntityRequest retrieveEntityRequest = new RetrieveEntityRequest();
            RetrieveEntityResponse retrieveEntityResponse = new RetrieveEntityResponse();

            retrieveEntityRequest.LogicalName = entityLogicalName;
            retrieveEntityRequest.EntityFilters = Microsoft.Xrm.Sdk.Metadata.EntityFilters.Attributes;

            retrieveEntityResponse = (RetrieveEntityResponse)orgService.Execute(retrieveEntityRequest);

            return retrieveEntityResponse.EntityMetadata.Attributes;
        }

        /// <summary>
        /// Returns the OptionMetadataCollection for the specified entity and attribute.
        /// </summary>
        /// <param name="orgService"></param>
        /// <param name="entityLogicalName"></param>
        /// <param name="attributeName"></param>
        /// <returns></returns>
        public static OptionMetadataCollection RetrieveOptionSetMetaDataCollection(this IOrganizationService orgService, string entityLogicalName, string attributeName)
        {
            OptionMetadataCollection returnOptionsCollection = null;
            RetrieveEntityRequest retrieveEntityRequest = new RetrieveEntityRequest();
            RetrieveEntityResponse retrieveEntityResponse = new RetrieveEntityResponse();

            retrieveEntityRequest.LogicalName = entityLogicalName;
            retrieveEntityRequest.EntityFilters = Microsoft.Xrm.Sdk.Metadata.EntityFilters.Attributes;

            retrieveEntityResponse = (RetrieveEntityResponse)orgService.Execute(retrieveEntityRequest);

            AttributeMetadata[] attributeMetadataArray = RetrieveAttributeMetaData(orgService, entityLogicalName);

            foreach (AttributeMetadata attributeMetadata in attributeMetadataArray)
            {
                if (attributeMetadata.AttributeType == AttributeTypeCode.Picklist &&
                    attributeMetadata.LogicalName == attributeName)
                {
                    returnOptionsCollection = ((PicklistAttributeMetadata)attributeMetadata).OptionSet.Options;
                    break;
                }
                else if (attributeMetadata.AttributeType == AttributeTypeCode.Status &&
                    attributeMetadata.LogicalName == attributeName)
                {
                    returnOptionsCollection = ((StatusAttributeMetadata)attributeMetadata).OptionSet.Options;
                    break;
                }
                else if (attributeMetadata.AttributeType == AttributeTypeCode.State &&
                    attributeMetadata.LogicalName == attributeName)
                {
                    returnOptionsCollection = ((StateAttributeMetadata)attributeMetadata).OptionSet.Options;
                    break;
                }
            }

            return returnOptionsCollection;
        }


    }

    /// <summary>
    /// Extensions for the Xrm Entity class to provide a set of common functions for manipulating Entity records.
    /// </summary>
    internal static class EntityExtension
    {
        /// <summary>
        /// Adds the specified attribute, or updates the value if the attribute exists.
        /// </summary>
        /// <param name="Target"></param>
        /// <param name="AttributeName"></param>
        /// <param name="Value"></param>
        public static void AddUpdateAttribute(this Entity Target, string AttributeName, object Value)
        {
            if (Target.Attributes.Contains(AttributeName))
            {
                Target.Attributes[AttributeName] = Value;
            }
            else
            {
                Target.Attributes.Add(AttributeName, Value);
            }
        }


        /// <summary>
        /// Adds an attribute to the target entity by copying a specified attribute from a source entity. If 
        /// the target already contains the enitity then the target attribute will be updated.
        /// </summary>
        /// <param name="Target"></param>
        /// <param name="TargetAttributeName"></param>
        /// <param name="Source"></param>
        /// <param name="SourceAttributeName"></param>
        public static void AddUpdateAttributeFromSource(this Entity Target, string TargetAttributeName, Entity Source, string SourceAttributeName)
        {
            if (Source.Contains(SourceAttributeName))
            {
                object value = Source[SourceAttributeName];
                Target.AddUpdateAttribute(TargetAttributeName, value);
            }
        }

        /// <summary>
        /// Checks the target for existance of any attribute contained in the provided array of attribute names and returns
        /// true if at least one of the provided attributes exists.
        /// </summary>
        /// <param name="Target"></param>
        /// <param name="AttributeNames"></param>
        /// <returns></returns>
        public static bool ContainsAny(this Entity Target, string[] AttributeNames)
        {
            foreach (string a in AttributeNames)
            {
                if (Target.Contains(a))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Retrieves an attribute from the entity with of the specified type and returns a 
        /// default value if the attribute does not exist.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="Target"></param>
        /// <param name="Key"></param>
        /// <param name="DefaultValue"></param>
        /// <returns></returns>
        public static T GetValue<T>(this Entity Target, string Key, T DefaultValue)
        {
            if (!Target.Contains(Key))
            {
                return DefaultValue;
            }
            else if (Target[Key] == null)
            {
                return DefaultValue;
            }
            else
            {
                return Target.GetAttributeValue<T>(Key);
            }
        }


        /// <summary>
        /// Merge the entity with another entity to get a more complete list of attributes. If 
        /// the current entity and the source entity both have a value for a given attribute 
        /// then then attribute of the current entity will be preserved.
        /// </summary>
        /// <param name="copyTo"></param>
        /// <param name="copyFrom"></param>
        public static void MergeWith(this Entity copyTo, Entity copyFrom)
        {
            if (copyTo != null && copyFrom != null)
            {
                copyFrom.Attributes.ToList().ForEach(a =>
                {
                    // if it already exists then dont copy
                    if (!copyTo.Attributes.ContainsKey(a.Key))
                    {
                        copyTo.Attributes.Add(a.Key, a.Value);
                    }
                });
            }
        }


        /// <summary>
        /// Removes an attribute from the entity attribute collection if that attribute exists.
        /// </summary>
        /// <param name="Target"></param>
        /// <param name="AttributeName"></param>
        public static void RemoveAttribute(this Entity Target, string AttributeName)
        {
            if (Target.Contains(AttributeName))
            {
                Target.Attributes.Remove(AttributeName);
            }
        }
    }
}

